package bg.tu.varna.pms.client;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.PriorityQueue;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
import org.omg.CORBA.portable.ApplicationException;
import bg.tu.varna.pms.model.communication.LoginMessage;
import bg.tu.varna.pms.model.communication.LogoutMessage;
import bg.tu.varna.pms.model.communication.RegisterMessage;
import bg.tu.varna.pms.model.communication.ServerInfoMessage;

class ServerReference {
	private static ServerReference mServerReference = new ServerReference();
	private Socket mServerSocket;
	private ObjectInputStream mIn;
	private ObjectOutputStream mOut;
	private int mSocketTimeout;
	private long mLastComm;
	private final PriorityQueue<PriorityMessage> mMessagePriorityQueue;

	final Vector<ResponseHandler<LoginMessage.Response>> mLoginResponseHandlerListeners;
	final Vector<ResponseHandler<LogoutMessage.Response>> mLogoutResponseHandlerListeners;
	final Vector<ResponseHandler<RegisterMessage.Response>> mRegisterResponseHandlerListeners;
	final ArrayList<Object> mResponseBuffer;
	final HashMap<Class<? extends Serializable>, ArrayList<ResponseHandler<? extends Serializable>>> mResponseHandlerListeners;

	private Thread mReceiverThread;
	private Runnable mReceiver;
	private Thread mTimeoutBreakerThread;
	private Runnable mTimeoutBreaker;
	private Thread mMessageQueueThread;
	private Runnable mMessageQueue;

	private ServerReference() {
		mLoginResponseHandlerListeners = new Vector<>( 1 );
		mLogoutResponseHandlerListeners = new Vector<>( 1 );
		mRegisterResponseHandlerListeners = new Vector<>( 1 );
		mResponseHandlerListeners = new HashMap<>();
		mResponseBuffer = new ArrayList<Object>();
		mMessagePriorityQueue = new PriorityQueue<>( 20, new PriorityMessage.PriorityMessageComparator() );
		mReceiver = new Runnable() {
			@Override
			public void run() {
				Object vTmp;
				try {
					while ( ( vTmp = mIn.readObject() ) != null ) {
						if ( vTmp instanceof LoginMessage.Response && !mLoginResponseHandlerListeners.isEmpty() ) {
							for ( ResponseHandler<LoginMessage.Response> vListener : mLoginResponseHandlerListeners ) {
								vListener.handleResponse( (LoginMessage.Response) vTmp );
							}
						} else if ( vTmp instanceof LogoutMessage.Response && !mLogoutResponseHandlerListeners.isEmpty() ) {
							for ( ResponseHandler<LogoutMessage.Response> vListener : mLogoutResponseHandlerListeners ) {
								vListener.handleResponse( (LogoutMessage.Response) vTmp );
							}
						} else if ( vTmp instanceof RegisterMessage.Response && !mRegisterResponseHandlerListeners.isEmpty() ) {
							for ( ResponseHandler<RegisterMessage.Response> vListener : mRegisterResponseHandlerListeners ) {
								vListener.handleResponse( (RegisterMessage.Response) vTmp );
							}
						} else if ( vTmp instanceof ServerInfoMessage ) {
							mSocketTimeout = ( (ServerInfoMessage) vTmp ).getSocketTimeout();
							if ( mSocketTimeout == 0 && mTimeoutBreakerThread != null ) {
								try {
									mTimeoutBreakerThread.interrupt();
									mTimeoutBreakerThread.join();
								} catch ( InterruptedException e ) {
									e.printStackTrace( System.err );
								} finally {
									mTimeoutBreakerThread = null;
								}
							} else if ( mSocketTimeout > 0 && mTimeoutBreakerThread == null ) {
								mTimeoutBreakerThread = new Thread( mTimeoutBreaker );
								mTimeoutBreakerThread.start();
							}
						} else if ( vTmp instanceof String ) {
							// timeout breaker
						} else {
							mResponseBuffer.add( vTmp );
						}
						commUpdate();
					}
				} catch ( ClassNotFoundException e ) {
					// TODO Auto-generated catch block
					e.printStackTrace( System.err );
				} catch ( IOException e ) {
					// TODO Auto-generated catch block
					e.printStackTrace( System.err );
				}
			}
		};
		mTimeoutBreaker = new Runnable() {
			@Override
			public void run() {
				while ( true ) {
					try {
						Thread.sleep( mSocketTimeout >> 1 );
					} catch ( InterruptedException e ) {
						break;
					}
					if ( ( GregorianCalendar.getInstance().getTimeInMillis() - mLastComm ) > ( mSocketTimeout >> 1 ) ) {
						while ( true ) {
							try {
								mOut.writeObject( "PING" );
							} catch ( IOException e ) {
								e.printStackTrace( System.err );
								continue;
							}
							break;
						}
					}
				}
			}
		};
		mMessageQueue = new Runnable() {
			@Override
			public void run() {
				while ( true ) {
					try {
						Thread.sleep( 500 );
						PriorityMessage vMessage = mMessagePriorityQueue.poll();
						if ( vMessage != null ) {
							mOut.writeObject( vMessage.getMessage() );
						}
					} catch ( InterruptedException e ) {
						break;// e.printStackTrace();
					} catch ( IOException e ) {
						e.printStackTrace( System.err );
					}
				}
			}
		};
	}

	public static ServerReference getInstance() {
		return mServerReference;
	}

	<T extends Serializable> void addResponseHandler( Class<T> type, ResponseHandler<T> responseHandler ) {
		if ( mResponseHandlerListeners.containsKey( type ) ) {
			mResponseHandlerListeners.get( type ).add( responseHandler );
		} else {
			ArrayList<ResponseHandler<? extends Serializable>> vTmp = new ArrayList<>();
			vTmp.add( responseHandler );
			mResponseHandlerListeners.put( type, vTmp );
		}
	}

	<T extends Serializable> void removeResponseHandler( Class<T> type, ResponseHandler<T> responseHandler ) {
		if ( mResponseHandlerListeners.containsKey( type ) ) {
			ArrayList<ResponseHandler<? extends Serializable>> vTmp = mResponseHandlerListeners.get( type );
			if ( vTmp.size() == 1 ) {
				mResponseHandlerListeners.remove( type );
			} else {
				vTmp.remove( responseHandler );
			}
		} else {
			// throw maybe
		}
	}

	public void queueMessage( int priority, Object message ) {
		mMessagePriorityQueue.add( new PriorityMessage( priority, message ) );
	}

	private void commSetup() throws ApplicationException {
		if ( mServerSocket == null ) {
			try {
				mServerSocket = new Socket( UserCache.getAppProp_Host(), UserCache.getAppProp_Port() );
				mIn = new ObjectInputStream( mServerSocket.getInputStream() );
				mOut = new ObjectOutputStream( mServerSocket.getOutputStream() );
				mReceiverThread = new Thread( mReceiver );
				mReceiverThread.start();
				mMessageQueueThread = new Thread( mMessageQueue );
				mMessageQueueThread.start();
				mLastComm = 0L;
			} catch ( UnknownHostException e ) {
				e.printStackTrace( System.err );
				throw new ApplicationException( "Server could not be found.", null );
			} catch ( IOException e ) {
				e.printStackTrace( System.err );
				throw new ApplicationException( "Server could not be contacted.", null );
			}
		}
	}

	private void commCleanup() throws ApplicationException {
		try {
			mMessageQueueThread.interrupt();
			mReceiverThread.interrupt();
			mReceiverThread.join();
			mServerSocket.shutdownInput();
			mServerSocket.shutdownOutput();
			mServerSocket.close();
		} catch ( InterruptedException e ) {
			e.printStackTrace( System.err );
			throw new ApplicationException( "ServerReference interrupted before completing the shutdown.", null );
		} catch ( IOException e ) {
			e.printStackTrace( System.err );
			throw new ApplicationException( "Cannot shutdown socket IO.", null );
		} finally {
			mMessageQueueThread = null;
			mReceiverThread = null;
			mIn = null;
			mOut = null;
			mServerSocket = null;
		}
	}

	private void commUpdate() {
		mLastComm = GregorianCalendar.getInstance().getTimeInMillis();
	}

	public LoginMessage.Response login( String username, char[] password )
			throws ApplicationException {
		commSetup();

		LoginMessage.Request vRequest = new LoginMessage.Request();
		LoginMessage.Response vResponse;

		vRequest.setUsername( username ).setPassword( password );

		CountDownLatch vLatch = new CountDownLatch( 1 );
		ResponseHandler<LoginMessage.Response> vHandler = new ResponseHandlerImpl<LoginMessage.Response>( vLatch, mLoginResponseHandlerListeners, 10000 );

		try {
			new Thread( vHandler ).run();
			mOut.writeObject( vRequest );
			commUpdate();
			vLatch.await();
		} catch ( InterruptedException e ) {
			// TODO Auto-generated catch block
			e.printStackTrace( System.err );
		} catch ( IOException e ) {
			e.printStackTrace( System.err );
			throw new ApplicationException( "Cannot make login request to server.", null );
		}

		vResponse = vHandler.getResponse();

		if ( vResponse == null ) {
			throw new ApplicationException( "Login request timeout.", null );
		}

		return vResponse;
	}

	public boolean logout( UUID sessionID, String username )
			throws ApplicationException {
		LogoutMessage.Request vRequest = new LogoutMessage.Request();
		LogoutMessage.Response vResponse;

		vRequest.setSessionID( sessionID ).setUsername( username );

		CountDownLatch vLatch = new CountDownLatch( 1 );
		ResponseHandler<LogoutMessage.Response> vHandler = new ResponseHandlerImpl<LogoutMessage.Response>( vLatch, mLogoutResponseHandlerListeners, 10000 );

		try {
			new Thread( vHandler ).run();
			mOut.writeObject( vRequest );
			commUpdate();
			vLatch.await();
		} catch ( InterruptedException e ) {
			// TODO Auto-generated catch block
			e.printStackTrace( System.err );
		} catch ( IOException e ) {
			e.printStackTrace( System.err );
			throw new ApplicationException( "Cannot make logout request to server.", null );
		}

		vResponse = vHandler.getResponse();

		if ( vResponse == null ) {
			return false;
		} else {
			commCleanup();
			return true;
		}
	}

	public boolean register( String username, char[] password )
			throws ApplicationException {
		commSetup();

		RegisterMessage.Request vRequest = new RegisterMessage.Request();
		RegisterMessage.Response vResponse;

		vRequest.setUsername( username ).setPassword( password );

		CountDownLatch vLatch = new CountDownLatch( 1 );
		ResponseHandler<RegisterMessage.Response> vHandler = new ResponseHandlerImpl<RegisterMessage.Response>( vLatch, mRegisterResponseHandlerListeners, 10000 );

		try {
			new Thread( vHandler ).run();
			mOut.writeObject( vRequest );
			commUpdate();
			vLatch.await();
		} catch ( InterruptedException e ) {
			// TODO Auto-generated catch block
			e.printStackTrace( System.err );
		} catch ( IOException e ) {
			e.printStackTrace( System.err );
			throw new ApplicationException( "Cannot register.", null );
		}

		vResponse = vHandler.getResponse();

		if ( vResponse == null ) {
			throw new ApplicationException( "Register request timeout.", null );
		}

		commCleanup();

		return vResponse.isSuccess();
	}

	static interface ResponseHandler<T> extends Runnable {
		void handleResponse( T response );
		T getResponse();
	}

	static class ResponseHandlerImpl<T> implements ResponseHandler<T> {
		T mResponse;
		final CountDownLatch mLatch;
		final Vector<ResponseHandler<T>> mListeners;
		final long mMillis;

		public ResponseHandlerImpl( final CountDownLatch latch, final Vector<ResponseHandler<T>> listeners, long millis ) {
			mLatch = latch;
			mListeners = listeners;
			mMillis = millis;
		}

		@Override
		public void run() {
			mListeners.add( this );
			for ( int i = 0; i < mMillis / 500; i++ ) {
				try {
					Thread.sleep( 500 );
					if ( mResponse != null ) {
						break;
					}
				} catch ( InterruptedException e ) {
					mListeners.remove( this );
					mLatch.countDown();
					return;
				}
			}
			mListeners.remove( this );
			mLatch.countDown();
		}

		@Override
		public void handleResponse( T response ) {
			mResponse = response;
			Thread.currentThread().interrupt();
		}

		@Override
		public T getResponse() {
			return mResponse;
		}

	}

	private final static class PriorityMessage {
		private final int mPriority;
		private final Object mMessage;

		public PriorityMessage( int priority, Object message ) {
			mPriority = priority;
			mMessage = message;
		}

		public Object getMessage() {
			return mMessage;
		}

		public final static class PriorityMessageComparator implements Comparator<PriorityMessage> {

			@Override
			public int compare( PriorityMessage x, PriorityMessage y ) {
				if ( x.mPriority < y.mPriority ) {
					return -1;
				} else if ( x.mPriority > y.mPriority ) {
					return 1;
				} else {
					return 0;
				}
			}

		}
	}
}
